package com.lmy.controllerr;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping(value = "/redisTest")
public class RedisTestController {

    @Resource
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "/t1", method = RequestMethod.GET)
    public String t1() {

        //设置字符串到redis
        redisTemplate.opsForValue().set("book", "天龙八部");

        //从redis获取值
        String book = (String) redisTemplate.opsForValue().get("book");
        return book;
    }

    @RequestMapping(value = "/t2", method = RequestMethod.GET)
    public String t2() {

        //设置list到redis
        redisTemplate.opsForList().leftPushAll("books", "笑傲江湖", "hello java");

        //从redis获取值
        List books = redisTemplate.opsForList().range("books", 0, -1);
        StringBuilder bookStr = new StringBuilder();
        for (Object book : books) {
            System.out.println("book-->" + book.toString());
            bookStr.append(book.toString()).append(",");
        }
        bookStr.deleteCharAt(bookStr.length() - 1);
        return bookStr.toString();
    }

    @RequestMapping(value = "/t3", method = RequestMethod.GET)
    public String t3() {

        //获取所有key
        Set keys = redisTemplate.keys("*");
        for (Object key : keys) {
            System.out.println("key-->" + key.toString());
        }
        return "ok";
    }

    //使用redis分布式锁完成key=num的自增操作
    @GetMapping("/lock")
    public void lock() {
        //1.获取锁/设置锁 执行setnx，为了防止死锁，给锁加上过期时间
        //为了防止锁的误删，给锁的值设置UUID保证唯一
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
        if (lock) {
            //说明锁设置成功，即获取到锁
            //这个key为num的数据，事先要在redis中初始化
            Object value = redisTemplate.opsForValue().get("num");
            //判断返回的value是否有值
            if (value == null || !StringUtils.hasText(value.toString())) {
                return;
            }

            //有值，就+1
            int num = Integer.parseInt(value.toString());
            redisTemplate.opsForValue().set("num", ++num);

            //释放锁-lock
            //为了防止误删其他用户的锁，先判断当前锁是不是前面获取到的锁
            //如果相同再删除锁/释放锁
            if (uuid.equals(redisTemplate.opsForValue().get("lock"))) {
                redisTemplate.delete("lock");
            }
        } else {
            //获取锁失败，休眠100ms，再重新获取锁/设置锁
            try {
                Thread.sleep(100);
                this.lock();    //重新执行
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    //使用LUA脚本实现删除锁操作的原子性
    /*
        为了确保分布式锁可用，要确保锁的实现同时满足以下四个条件
        1.互斥性。在任意时刻，只有一个客户端能持有锁
        2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁，也能保证后续其他客户端能加锁
        3.加锁和解锁必须是同一个客户端，A客户端不能把B客户端加的锁给解了
        4.加锁和解锁必须具有原子性
     */
    @GetMapping(value = "/lockByLua")
    public void lockByLua() {
        //1.获取锁/设置锁 执行setnx，为了防止死锁，给锁加上过期时间
        //为了防止锁的误删，给锁的值设置UUID保证唯一
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
        if (lock) {
            //说明锁设置成功，即获取到锁
            //这个key为num的数据，事先要在redis中初始化
            Object value = redisTemplate.opsForValue().get("num");
            //判断返回的value是否有值
            if (value == null || !StringUtils.hasText(value.toString())) {
                return;
            }

            //有值，就+1
            int num = Integer.parseInt(value.toString());
            redisTemplate.opsForValue().set("num", ++num);

            //释放锁-lock
            //使用LUA脚本控制锁删除的原子性
            String script = "if redis.call('get', KEYS[1])==ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            //设置返回值类型为Long
            //因为删除判断的时候，返回的0给其封装为数据类型。如果不封装那么默认返回String，那么返回字符串与0会有发生错误
            redisScript.setResultType(Long.class);
            /*
                第一个参数是要执行的LUA脚本
                第二个参数是需要判断的key
                第三个参数是key所对应的值
                Arrays.asList("lock")会传递给script的KEYS[1],uuid会传递给ARGV[1]
             */
            redisTemplate.execute(redisScript, Arrays.asList("lock"), uuid);
        } else {
            //获取锁失败，休眠100ms，再重新获取锁/设置锁
            try {
                Thread.sleep(100);
                this.lock();    //重新执行
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
